<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
    />
    <meta
      name="description"
      content="Rendering Volume Cloud with Texture3D and Custom GLSL. Transplanted from Three.js"
    />
    <meta name="cesium-sandcastle-labels" content="Tutorials,Showcases" />
    <title>Cesium Demo</title>
    <script type="text/javascript" src="../Sandcastle-header.js"></script>
    <script
      type="text/javascript"
      src="../../../Build/CesiumUnminified/Cesium.js"
      nomodule
    ></script>
    <script type="module" src="../load-cesium-es6.js"></script>
  </head>

  <body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
    <style>
      @import url(../templates/bucket.css);
    </style>
    <div id="cesiumContainer" class="fullSize"></div>
    <div id="loadingOverlay">
      <h1>Loading...</h1>
    </div>
    <div id="toolbar"></div>
    <script id="cesium_sandcastle_script">
      window.startup = async function (Cesium) {
        "use strict";
        //Sandcastle_Begin

        // ImprovedNoise from Three.js
        // https://github.com/mrdoob/three.js/blob/dev/examples/jsm/math/ImprovedNoise.js

        const lerp = Cesium.Math.lerp;

        const _p = [
          151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36,
          103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0,
          26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56,
          87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
          77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55,
          46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132,
          187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109,
          198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126,
          255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183,
          170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172,
          9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
          218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81,
          51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84,
          204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67,
          29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
        ];

        for (let i = 0; i < 256; i++) {
          _p[256 + i] = _p[i];
        }

        function fade(t) {
          return t * t * t * (t * (t * 6 - 15) + 10);
        }

        function grad(hash, x, y, z) {
          const h = hash & 15;
          const u = h < 8 ? x : y,
            v = h < 4 ? y : h === 12 || h === 14 ? x : z;
          return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
        }

        /**
         * A utility class providing a 3D noise function.
         *
         * The code is based on [IMPROVED NOISE]{@link https://cs.nyu.edu/~perlin/noise/}
         * by Ken Perlin, 2002.
         *
         * @three_import import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
         */
        class ImprovedNoise {
          /**
           * Returns a noise value for the given parameters.
           *
           * @param {number} x - The x coordinate.
           * @param {number} y - The y coordinate.
           * @param {number} z - The z coordinate.
           * @return {number} The noise value.
           */
          noise(x, y, z) {
            const floorX = Math.floor(x),
              floorY = Math.floor(y),
              floorZ = Math.floor(z);

            const X = floorX & 255,
              Y = floorY & 255,
              Z = floorZ & 255;

            x -= floorX;
            y -= floorY;
            z -= floorZ;

            const xMinus1 = x - 1,
              yMinus1 = y - 1,
              zMinus1 = z - 1;

            const u = fade(x),
              v = fade(y),
              w = fade(z);

            const A = _p[X] + Y,
              AA = _p[A] + Z,
              AB = _p[A + 1] + Z,
              B = _p[X + 1] + Y,
              BA = _p[B] + Z,
              BB = _p[B + 1] + Z;

            return lerp(
              lerp(
                lerp(grad(_p[AA], x, y, z), grad(_p[BA], xMinus1, y, z), u),
                lerp(grad(_p[AB], x, yMinus1, z), grad(_p[BB], xMinus1, yMinus1, z), u),
                v,
              ),
              lerp(
                lerp(
                  grad(_p[AA + 1], x, y, zMinus1),
                  grad(_p[BA + 1], xMinus1, y, zMinus1),
                  u,
                ),
                lerp(
                  grad(_p[AB + 1], x, yMinus1, zMinus1),
                  grad(_p[BB + 1], xMinus1, yMinus1, zMinus1),
                  u,
                ),
                v,
              ),
              w,
            );
          }
        }

        // End ImprovedNoise from Three.js

        // GeometryPrimitive
        const {
          Cartesian3,
          destroyObject,
          DrawCommand,
          VertexArray,
          GeometryPipeline,
          Matrix4,
        } = Cesium;

        /**
         * Custom Primitive for Geometry
         */
        class GeometryPrimitive {
          /**
           *
           * @param {*} options
           * @param {*} options.modelMatrix
           * @param {*} options.vertexShaderSource
           * @param {*} options.fragmentShaderSource
           * @param {*} options.uniformMap
           * @param {*} options.renderState
           * @param {*} options.pass
           */
          constructor(geometry, options) {
            this.options = options;
            this.geometry = geometry;
          }

          /**
           *
           * @param {*} frameState
           */
          update(frameState) {
            if (Cesium.defined(this._drawCommand)) {
              frameState.commandList.push(this._drawCommand);
              return;
            }
            if (this.geometry.constructor.createGeometry) {
              this.geometry = this.geometry.constructor.createGeometry(this.geometry);
            }

            const context = frameState.context;

            const attributeLocations = GeometryPipeline.createAttributeLocations(
              this.geometry,
            );
            const vertexArray = VertexArray.fromGeometry({
              context: context,
              geometry: this.geometry,
              attributeLocations,
            });

            // calculate boundingSphere
            const boundingSphere = this.geometry.boundingSphere;
            boundingSphere.center = Matrix4.multiplyByPoint(
              this.options.modelMatrix,
              boundingSphere.center,
              new Cartesian3(),
            );
            boundingSphere.radius = 1000000;

            this._boundingSphereWC = [boundingSphere];

            const shaderProgram = Cesium.ShaderProgram.fromCache({
              context: context,
              attributeLocations,
              vertexShaderSource: this.options.vertexShaderSource,
              fragmentShaderSource: this.options.fragmentShaderSource,
            });

            this._drawCommand = new DrawCommand({
              owner: this,
              boundingVolume: boundingSphere,
              primitiveType: this.geometry.primitiveType,
              vertexArray: vertexArray,
              shaderProgram,
              ...this.options,
            });
          }

          destroy() {
            return destroyObject(this);
          }

          isDestroyed() {
            return false;
          }
        }
        // GeometryPrimitive end

        function makeTexture3D(context) {
          const size = 100;
          const dataLength = size * size * size;
          const data = new Uint8Array(dataLength);
          let i = 0;
          const scale = 0.05;
          const perlin = new ImprovedNoise();
          let vector = new Cesium.Cartesian3();
          const halfSize = Cesium.Cartesian3.fromElements(
            size / 2,
            size / 2,
            size / 2,
            new Cesium.Cartesian3(),
          );

          for (let z = 0; z < size; z++) {
            for (let y = 0; y < size; y++) {
              for (let x = 0; x < size; x++) {
                vector = Cesium.Cartesian3.fromElements(x, y, z, vector);
                vector = Cesium.Cartesian3.subtract(vector, halfSize, vector);
                vector = Cesium.Cartesian3.divideByScalar(vector, size, vector);

                const d = 1.0 - Cesium.Cartesian3.magnitude(vector);
                const tv =
                  (128 +
                    128 * perlin.noise((x * scale) / 1.5, y * scale, (z * scale) / 1.5)) *
                  d *
                  d;

                data[i] = tv;
                i++;
              }
            }
          }

          return new Cesium.Texture3D({
            context: context,
            width: size,
            height: size,
            depth: size,
            flipY: false,
            pixelFormat: Cesium.PixelFormat.RED,
            pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
            source: {
              arrayBufferView: data,
              width: size,
              height: size,
              depth: size,
            },
            sampler: new Cesium.Sampler({
              minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
              magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
            }),
          });
        }

        const vertexShader = /* glsl */ `
      in vec3 position3DHigh;
      in vec3 position3DLow;
      in vec3 normal;
      in vec2 st;
      in float batchId;

      out vec3 vOrigin;
      out vec3 vDirection;
      out vec3 vPosition;

      vec4 translateRelativeToEye(vec3 high, vec3 low) {
          vec3 highDifference = high - czm_encodedCameraPositionMCHigh;
          if(length(highDifference) == 0.0f) {
              highDifference = vec3(0);
          }
          vec3 lowDifference = low - czm_encodedCameraPositionMCLow;
          return vec4(highDifference + lowDifference, 1.0f);
      }

      void main()
      {
          vec4 p = translateRelativeToEye(position3DHigh, position3DLow);
          
          vOrigin = czm_encodedCameraPositionMCHigh + czm_encodedCameraPositionMCLow;

          vec3 modelPosition = position3DHigh + position3DLow;
          vPosition = modelPosition;

          vDirection = modelPosition - vOrigin;
 
          gl_Position = czm_modelViewProjectionRelativeToEye * p;
      }`;

        const fragmentShader = /* glsl */ `
      precision highp float;
      precision highp sampler3D;

      in vec3 vOrigin;
      in vec3 vDirection;

      // https://github.com/mrdoob/three.js/blob/dev/examples/webgl_volume_cloud.html
      uniform vec3 base;
      uniform sampler3D map;
			uniform float threshold;
			uniform float range;
			uniform float opacity;
			uniform float steps;
			uniform float frame;

			uint wang_hash(uint seed)
			{
					seed = (seed ^ 61u) ^ (seed >> 16u);
					seed *= 9u;
					seed = seed ^ (seed >> 4u);
					seed *= 0x27d4eb2du;
					seed = seed ^ (seed >> 15u);
					return seed;
			}

			float randomFloat(inout uint seed)
			{
					return float(wang_hash(seed)) / 4294967296.;
			}

			vec2 hitBox( vec3 orig, vec3 dir ) {
				const vec3 box_min = vec3( - 0.5 );
				const vec3 box_max = vec3( 0.5 );
				vec3 inv_dir = 1.0 / dir;
				vec3 tmin_tmp = ( box_min - orig ) * inv_dir;
				vec3 tmax_tmp = ( box_max - orig ) * inv_dir;
				vec3 tmin = min( tmin_tmp, tmax_tmp );
				vec3 tmax = max( tmin_tmp, tmax_tmp );
				float t0 = max( tmin.x, max( tmin.y, tmin.z ) );
				float t1 = min( tmax.x, min( tmax.y, tmax.z ) );
				return vec2( t0, t1 );
			}

		  float sample1( vec3 p ) {
		  	return texture( map, p ).r;
		  }

		  float shading( vec3 coord ) {
		  	float step = 0.01;
		  	return sample1( coord + vec3( - step ) ) - sample1( coord + vec3( step ) );
		  }

		  vec4 linearToSRGB( in vec4 value ) {
		  	return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
		  }

      void main() {
					vec3 rayDir = normalize( vDirection );
					vec2 bounds = hitBox( vOrigin, rayDir );
					if ( bounds.x > bounds.y ) discard;
					bounds.x = max( bounds.x, 0.0 );
					vec3 p = vOrigin + bounds.x * rayDir;
					vec3 inc = 1.0 / abs( rayDir );
					float delta = min( inc.x, min( inc.y, inc.z ) );
					delta /= steps;

					// Nice little seed from
					// https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/
					uint seed = uint( gl_FragCoord.x ) * uint( 1973 ) + uint( gl_FragCoord.y ) * uint( 9277 ) + uint( frame ) * uint( 26699 );
					vec3 size = vec3( textureSize( map, 0 ) );
					float randNum = randomFloat( seed ) * 2.0 - 1.0;
					p += rayDir * randNum * ( 1.0 / size );

					vec4 ac = vec4( base, 0.0 );
					for ( float t = bounds.x; t < bounds.y; t += delta ) {
						float d = sample1( p + 0.5 );
						d = smoothstep( threshold - range, threshold + range, d ) * opacity;
						float col = shading( p + 0.5 ) * 3.0 + ( ( p.x + p.y ) * 0.25 ) + 0.2;
						ac.rgb += ( 1.0 - ac.a ) * d * col;
						ac.a += ( 1.0 - ac.a ) * d;
						if ( ac.a >= 0.95 ) break;
						p += rayDir * delta;
					}
					vec4 color = linearToSRGB( ac );
          color = czm_gammaCorrect( color );
					if ( color.a == 0.0 ) discard;
          out_FragColor = color;
      }
      `;

        const viewer = new Cesium.Viewer("cesiumContainer", {
          orderIndependentTranslucency: true,
        });
        const texture3D = makeTexture3D(viewer.scene.context);
        texture3D.generateMipmap();

        const boxSideLength = 1.0;
        const zoomScale = 10000;
        const centerPoint = Cesium.Cartesian3.fromDegrees(
          113,
          33,
          (boxSideLength / 0.5) * zoomScale,
        );

        const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(centerPoint);

        const boxSize = new Cesium.Cartesian3(
          boxSideLength,
          boxSideLength,
          boxSideLength,
        );

        const renderState = Cesium.RenderState.fromCache({
          depthMask: false,
          blending: {
            enabled: true,
            color: {
              red: 0.0,
              green: 0.0,
              blue: 0.0,
              alpha: 0.0,
            },
          },
          depthTest: {
            enabled: true,
            func: Cesium.DepthFunction.LESS_OR_EQUAL,
          },
          cull: {
            enabled: true,
            face: Cesium.CullFace.FRONT,
          },
        });

        const zoomMat = Cesium.Matrix4.fromScale(
          new Cesium.Cartesian3(zoomScale, zoomScale, zoomScale),
          new Cesium.Matrix4(),
        );

        Cesium.Matrix4.multiply(modelMatrix, zoomMat, modelMatrix);

        const halfBox = Cesium.Cartesian3.multiplyByScalar(
          boxSize,
          0.5,
          new Cesium.Cartesian3(),
        );
        const negHalfBox = Cesium.Cartesian3.negate(halfBox, new Cesium.Cartesian3());

        const boxGeometry = new Cesium.BoxGeometry({
          minimum: negHalfBox,
          maximum: halfBox,
        });

        const uniforms = {
          base: new Cesium.Color(0.1912, 0.2542, 0.3515, 0),
          map: texture3D,
          opacity: 0.25,
          range: 0.1,
          steps: 100,
          frame: 0,
          threshold: 0.25,
        };

        window.uniforms = uniforms;

        const cmdUniforms = {};
        for (const key in uniforms) {
          if (key) {
            cmdUniforms[key] = function () {
              return uniforms[key];
            };
          }
        }

        const primitive = new GeometryPrimitive(boxGeometry, {
          uniformMap: cmdUniforms,
          vertexShaderSource: vertexShader,
          fragmentShaderSource: fragmentShader,
          renderState: renderState,
          modelMatrix,
          pass: Cesium.Pass.TRANSLUCENT,
        });

        viewer.scene.primitives.add(primitive);

        const cameraState = {
          destination: centerPoint,
          orientation: {
            heading: 4.159717744111784,
            pitch: -0.4648127266675117,
            roll: boxSideLength * zoomScale * 2,
          },
          duration: 0,
        };
        // viewer.camera.flyTo(camState);
        const hpr = new Cesium.HeadingPitchRange(
          cameraState.orientation.heading,
          cameraState.orientation.pitch,
          boxSideLength * zoomScale * 2,
        );
        viewer.camera.lookAt(cameraState.destination, hpr);

        // create params control UI
        function createSlider(
          toolbar,
          { labelText, min, max, step, defaultValue, callback },
        ) {
          const container = document.createElement("div");
          container.style.cssText = `
          display: flex;
          align-items: center;
          gap: 10px;
          margin-bottom: 8px;
        `;

          const label = document.createElement("label");
          label.textContent = labelText;
          label.style.cssText = `
          color: #ffffff;
          font-family: Arial, sans-serif;
          min-width: 60px;
          font-size: 14px;
        `;

          const slider = document.createElement("input");
          slider.type = "range";
          slider.min = min;
          slider.max = max;
          slider.step = step;
          slider.value = defaultValue;
          slider.style.cssText = `
          width: 100px;
          cursor: pointer;
          accent-color: #4CAF50;
        `;
          slider.addEventListener("input", (e) => callback(e.target.value));

          const valueDisplay = document.createElement("span");
          valueDisplay.textContent = defaultValue;
          valueDisplay.style.cssText = `
          color: #ffffff;
          font-family: monospace;
          width: 30px;
          text-align: right;
        `;
          slider.addEventListener("input", (e) => {
            valueDisplay.textContent = parseFloat(e.target.value).toFixed(2);
          });

          container.appendChild(label);
          container.appendChild(slider);
          container.appendChild(valueDisplay);
          toolbar.appendChild(container);

          return slider;
        }

        function createUi() {
          const toolbar = document.getElementById("toolbar");

          toolbar.style.cssText = `
          position: fixed;
          top: 10px;
          left: 10px;
          padding: 15px 20px 5px 20px;
          background: rgba(40, 40, 40, 0.85);
          border-radius: 12px;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
          backdrop-filter: blur(5px);
          z-index: 1000;
        `;

          createSlider(toolbar, {
            labelText: "opacity",
            min: 0,
            max: 1,
            step: 0.01,
            defaultValue: uniforms.opacity,
            callback: (val) => {
              uniforms.opacity = parseFloat(val);
            },
          });

          createSlider(toolbar, {
            labelText: "range",
            min: 0,
            max: 1,
            step: 0.01,
            defaultValue: uniforms.range,
            callback: (val) => {
              uniforms.range = parseFloat(val);
            },
          });
          createSlider(toolbar, {
            labelText: "steps",
            min: 20,
            max: 200,
            step: 1,
            defaultValue: uniforms.steps,
            callback: (val) => {
              uniforms.steps = parseFloat(val);
            },
          });
          createSlider(toolbar, {
            labelText: "threshold",
            min: 0,
            max: 1,
            step: 0.01,
            defaultValue: uniforms.threshold,
            callback: (val) => {
              uniforms.threshold = parseFloat(val);
            },
          });
        }

        createUi();

        //Sandcastle_End
      };
      if (typeof Cesium !== "undefined") {
        window.startupCalled = true;
        window.startup(Cesium).catch((error) => {
          "use strict";
          console.error(error);
        });
        Sandcastle.finishedLoading();
      }
    </script>
  </body>
</html>
